/*
 * Copyright (C) 2001-2006 the xine project
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
 *
 * $Id: preferences.c,v 1.61 2006/04/05 19:08:33 dsalt Exp $
 *
 * Functions to stup and deal with a preferences dialog interfacing with
 * the xine config system.
 *
 * Richard Wareham <richwareham@users.sourceforge.net> -- March 2002
 * Darren Salt <dsalt@users.sourceforge.net> -- December 2004
 */

#include "globals.h"

#include <assert.h>
#include <X11/Xlib.h>
#include <gtk/gtk.h>
#include <gdk/gdk.h>
#include <gdk/gdkkeysyms.h>
#include <glib.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "mediamarks.h"
#include "preferences.h"
#include "utils.h"

#include <jsapi.h>


typedef struct {
  void *notebook; /* NULL - get from parent */
  GtkWidget *editable, *label, *separator;
  int exp;
} pref_item_t;
#define PREF_ITEM_T(NODE) ((pref_item_t *)(NODE)->data)

typedef struct {
  GtkWidget *notebook; /* contains child pages, not this page */
  GtkWidget *page, *table;
  char *prefix, *label;
} pref_page_t;
#define PREF_PAGE_T(NODE) ((pref_page_t *)(NODE)->data)

typedef union {
  GtkWidget *notebook;
  pref_item_t item;
  pref_page_t page;
} pref_t;
#define PREF_T(NODE) ((pref_t *)(NODE)->data)

static GtkWidget  *prefs_dialog;
static GtkWidget  *bowdler_dialog;
static GNode	  *prefs = NULL;
static GData	  *prefs_map = NULL;
static int         is_visible;
static GtkTooltips *tooltips = NULL;
static int         update_lock = 0;


static int select_show_prefs_internal (GNode *page, int exp)
{
  int state = 0;
  pref_t *item = page->data;
  pref_t *parent = page->parent ? page->parent->data : NULL;
  GNode *child;

  if (parent && parent->notebook == GINT_TO_POINTER (1))
    return 0; /* called too early */

  child = g_node_first_child (page);

  if (PREF_T(child)->notebook)
  {
    /* recursively process subpages (but first we hide them all) */
    GtkNotebook *notebook = GTK_NOTEBOOK(item->page.notebook);
    while (gtk_notebook_get_n_pages (notebook))
      gtk_notebook_remove_page (notebook, 0);
    for (; child; child = g_node_next_sibling (child))
      state |= select_show_prefs_internal (child, exp);
  }
  else
  {
    /* hide/show widgets on this page */
    pref_item_t *last = NULL;
    for (; child; child = g_node_next_sibling (child))
    {
      pref_item_t *pref = child->data;
      void (*func)(GtkWidget *);
      if (pref->exp <= exp)
      {
        func = gtk_widget_show;
        state |= 1;
        last = pref;
      }
      else
      {
        func = gtk_widget_hide;
        state |= 2;
      }
      if (pref->exp <= exp)
	last = pref;
      func (pref->editable);
      func (pref->label);
      func (pref->separator);
    }
    if (last)
      gtk_widget_hide (last->separator);
  }

  /* re-add the page if it has visible widgets */
  if (parent && state != 2)
    gtk_notebook_append_page (GTK_NOTEBOOK(parent->notebook), item->page.page,
			      gtk_label_new (item->page.label));

  return state;
}

static gboolean select_show_pref_widgets (void *data)
{
  static const int experience_values[] = { 0, 10, 20, 30 };
  int exp = *(int *)data;

  if (exp >= 0 && exp < (int) G_N_ELEMENTS (experience_values))
    exp = experience_values[exp];
  else
    exp = 0;

  ++update_lock;
  gdk_threads_enter ();
  select_show_prefs_internal (prefs, exp);
  gdk_threads_leave ();
  --update_lock;
  return FALSE;
}

static gboolean entry_cb (GtkEntry *editable, GdkEventFocus *even,
			  gpointer user_data)
{
  xine_cfg_entry_t      entry;
  gchar                *key = (gchar *) user_data;

  if (update_lock)
    return FALSE;

  logprintf ("preferences: entry cb for key %s\n", key);

  if (!xine_config_lookup_entry (xine, key, &entry))
    return FALSE;

  key = gtk_editable_get_chars (GTK_EDITABLE(editable), 0, -1);
  if (!strcmp (entry.str_value, key))
    return FALSE;

  logprintf ("preferences: updating entry\n");
  entry.str_value = key;
  xine_config_update_entry (xine, &entry);

  return FALSE;
}

static gboolean entry_keypress_cb (GtkEntry *widget, GdkEventKey *event,
				   gpointer data)
{
  switch (event->keyval)
  {
  case GDK_Return:
  case GDK_KP_Enter:
    entry_cb (widget, NULL, data);
    return TRUE;

  case GDK_minus:
    if ((event->state & GXINE_MODIFIER_MASK) != GDK_CONTROL_MASK)
      break;
  case GDK_Undo:
    {
      const char *key = g_object_get_data (G_OBJECT(widget), "cfg");
      xine_cfg_entry_t entry;
      if (key && xine_config_lookup_entry (xine, key, &entry))
	gtk_entry_set_text (widget, entry.str_value);
    }
    return TRUE;
  }

  return FALSE;
}

static void check_box_cb (GtkToggleButton *togglebutton, gpointer user_data)
{
  xine_cfg_entry_t      entry;
  gchar                *key = (gchar *) user_data;
  int			state;

  if (update_lock)
    return;

  logprintf ("preferences: check box cb for key %s\n", key);

  if (!xine_config_lookup_entry (xine, key, &entry))
    return;

  state = gtk_toggle_button_get_active (togglebutton);
  if (entry.num_value == state)
    return;

  logprintf ("preferences: updating entry\n");
  entry.num_value = state;
  xine_config_update_entry (xine, &entry);
}

static void range_cb (GtkAdjustment *adj, const gchar *key)
{
  xine_cfg_entry_t      entry;

  if (update_lock)
    return;

  logprintf ("preferences: range cb for key %s\n", key);

  if (!xine_config_lookup_entry (xine, key, &entry) ||
      entry.num_value == adj->value)
    return;

  logprintf ("preferences: updating entry to %lf\n", adj->value);
  entry.num_value = adj->value;
  xine_config_update_entry (xine, &entry);
}

static void spin_cb (GtkSpinButton *widget, const gchar *key)
{
  xine_cfg_entry_t entry;
  int value;

  if (update_lock)
    return;

  logprintf ("preferences: spin cb for key %s\n", key);

  value = gtk_spin_button_get_value_as_int (widget);
  if (!xine_config_lookup_entry (xine, key, &entry) ||
      entry.num_value == value)
    return;

  logprintf ("preferences: updating entry to %d\n", value);
  entry.num_value = value;
  xine_config_update_entry (xine, &entry);
}

static void enum_cb (GtkWidget* widget, gpointer data)
{
  xine_cfg_entry_t      entry;
  gchar                *key = (gchar *) data;
  static int            pos;

  if (update_lock)
    return;

  logprintf ("preferences: enum cb for key %s\n", key);

  if (!xine_config_lookup_entry (xine, key, &entry))
    return;

  pos = gtk_combo_box_get_active (GTK_COMBO_BOX(widget));

  if (entry.num_value != pos)
  {
    entry.num_value = pos;
    logprintf ("preferences: updating entry to %d\n", pos);
    xine_config_update_entry (xine, &entry);
    if (!strcmp (key, "gui.experience_level"))
      g_idle_add ((GSourceFunc) select_show_pref_widgets, &pos);
  }
}


static GtkWidget *create_item_editable (const xine_cfg_entry_t *entry)
{
  GtkWidget *widget;

  switch (entry->type)
  {
  case XINE_CONFIG_TYPE_ENUM:
    {
      int i;
      widget = gtk_combo_box_new_text ();
      for (i = 0; entry->enum_values[i]; ++i)
      {
	const char *label = gettext (entry->enum_values[i]);
	if (label == entry->enum_values[i])
	  label = dgettext (LIB_PACKAGE, label);
	gtk_combo_box_append_text (GTK_COMBO_BOX(widget), label);
      }
      gtk_combo_box_set_active (GTK_COMBO_BOX(widget), entry->num_value);
      g_signal_connect (G_OBJECT(widget), "changed",
			G_CALLBACK(enum_cb), strdup(entry->key));
    }
    break;

  case XINE_CONFIG_TYPE_STRING:
    widget = gtk_entry_new();
    g_object_set_data (G_OBJECT(widget), "cfg", (gpointer)entry->key);
    g_object_connect (G_OBJECT(widget),
	"signal::key-press-event", G_CALLBACK(entry_keypress_cb), (gchar *)entry->key,
	"signal::focus-out-event", G_CALLBACK(entry_cb), (gchar *)entry->key,
	NULL);
    gtk_entry_set_text (GTK_ENTRY(widget), entry->str_value);
    break;

  case XINE_CONFIG_TYPE_RANGE: /* slider */
    {
      GtkObject *adj = gtk_adjustment_new (entry->num_value, entry->range_min,
					   entry->range_max, 1.0, 10.0, 0.0);
      widget = gtk_hscale_new (GTK_ADJUSTMENT(adj));
      gtk_scale_set_draw_value (GTK_SCALE(widget), TRUE);
      gtk_scale_set_value_pos (GTK_SCALE(widget), GTK_POS_TOP);
      gtk_scale_set_digits (GTK_SCALE(widget), 0);
      g_signal_connect (adj, "value-changed",
			G_CALLBACK (range_cb), (gchar *)entry->key);
    }
    break;

  case XINE_CONFIG_TYPE_NUM:
    {
      GtkObject *adj = gtk_adjustment_new (entry->num_value, INT_MIN, INT_MAX,
					   1, 10, 0);
      widget = gtk_spin_button_new (GTK_ADJUSTMENT(adj), 1, 0);
      gtk_spin_button_set_update_policy (GTK_SPIN_BUTTON(widget),
					 GTK_UPDATE_ALWAYS);
      gtk_spin_button_set_numeric (GTK_SPIN_BUTTON(widget), TRUE);
      g_signal_connect (G_OBJECT(widget), "value-changed",
			G_CALLBACK(spin_cb), (gchar *)entry->key);
    }
    break;

  case XINE_CONFIG_TYPE_BOOL:
    widget = gtk_check_button_new();
    gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON(widget),
				  entry->num_value == 1);
    g_signal_connect (G_OBJECT(widget), "toggled",
		      G_CALLBACK(check_box_cb), (gchar *)entry->key);
    /* gtk_misc_set_alignment (GTK_MISC(widget), 1.0, 1.0); */
    break;

  default:
    widget = gtk_label_new ("?");
    g_print (_("preferences: unknown type for entry ‘%s’\n"), entry->key);
  }

  g_datalist_set_data (&prefs_map, entry->key, widget);
  gtk_tooltips_set_tip (tooltips, widget, entry->help, NULL);
  return widget;
}

static GtkWidget *create_item_label (const xine_cfg_entry_t *entry)
{
  GtkWidget *label;
  char *labeltext;
  const char *labelkey = strrchr (entry->key, '.') + 1;

  if (entry->callback)
    labeltext = g_markup_printf_escaped ("<b>%s</b>\n%s",
					 labelkey, entry->description);
  else
    labeltext = g_markup_printf_escaped ("<b>• <i>%s</i></b>\n%s",
					 labelkey, entry->description);

  label = gtk_label_new (labeltext);
  free (labeltext);

  gtk_label_set_use_markup (GTK_LABEL(label), TRUE);
  gtk_label_set_line_wrap (GTK_LABEL(label), TRUE);
  gtk_label_set_justify (GTK_LABEL (label), GTK_JUSTIFY_LEFT);
  gtk_misc_set_alignment (GTK_MISC (label), 0, 0.5);

  return label;
}

static GNode *create_item (const xine_cfg_entry_t *entry)
{
  pref_item_t *item = malloc (sizeof (pref_item_t));
  item->notebook = NULL;
  item->editable = create_item_editable (entry);
  item->label = create_item_label (entry);
  item->separator = gtk_hseparator_new ();
  item->exp = entry->exp_level;
  return g_node_new (item);
}

static GNode *create_page (const char *key, size_t length)
{
  pref_page_t *page = malloc (sizeof (pref_page_t));
  page->notebook = GINT_TO_POINTER (1); /* dummy value - filled in later */
  page->page = page->table = NULL;
  page->prefix = malloc (length + 1);
  sprintf (page->prefix, "%.*s", (int) length, key);
  page->label = strrchr (page->prefix, '.');
  if (page->label)
    ++page->label;
  else
    page->label = page->prefix;
  return g_node_new (page);
}

struct page_find_s {
  const char *key;
  size_t length;
  GNode *match;
};

static gboolean find_page_sub (GNode *node, struct page_find_s *find)
{
  const pref_page_t *pref = node->data;
  if (!pref->notebook)
    return FALSE;
  if (strncmp (pref->prefix, find->key, find->length) ||
      pref->prefix[find->length])
    return FALSE;
  find->match = node;
  return TRUE;
}

static GNode *find_page (const char *key, size_t length)
{
  struct page_find_s find = { key, length, NULL };
  g_node_traverse (prefs, G_PRE_ORDER, G_TRAVERSE_ALL, -1,
		   (GNodeTraverseFunc) find_page_sub, &find);
  return find.match;
}

static gboolean unmix_mixed_page (GNode *node, gboolean *done)
{
  int types = 0;
  GNode *child, *new_page;
  char *prefix;
  pref_page_t *page = node->data;

  /* not a page? return */
  if (!page->notebook)
    return FALSE;

  /* check if this page has mixed content */
  for (child = g_node_first_child (node); child;
       child = g_node_next_sibling (child))
    types |= (PREF_T(child)->notebook ? 1 : 2);

  switch (types)
  {
  case 0:
    /* hmm, odd... found a blank page - delete it */
    logprintf ("gxine: prefs: eek, found a blank page '%s'\n", page->prefix);
    gtk_widget_destroy (page->notebook);
    free (page->prefix);
    free (page);
    g_node_destroy (node);
    *done = FALSE;
    return TRUE;

  case 3:
    /* found a mixed-mode page (this is normal) */
    prefix = g_strdup_printf ("%s. ", page->prefix);
    new_page = g_node_prepend (node, create_page (prefix, strlen (prefix)));
    free (prefix);

    /* move the new subpage's sibling item nodes into it */
    child = g_node_first_child (node);
    while (child)
    {
      GNode *next = g_node_next_sibling (child);
      pref_t *pref = child->data;
      if (!pref->notebook)
      {
	g_node_unlink (child);
	g_node_append (new_page, child);
      }
      child = next;
    }

    *done = FALSE;
    return TRUE;
  }

  return FALSE;
}

static gboolean focus_item_cb (GtkWidget *widget, GtkDirectionType *dir,
			       GNode *node)
{
  const pref_t *pref = node->data;
  const pref_t *parent = node->parent ? node->parent->data : NULL;
  GtkWidget *viewport = parent->page.table->parent;
  GtkAdjustment *adj = gtk_viewport_get_vadjustment ((GtkViewport *)viewport);

  int wt = pref->item.editable->allocation.y;
  int wb = wt + pref->item.editable->allocation.height;
  int lt = pref->item.label->allocation.y;
  int lb = lt + pref->item.label->allocation.height;
  int top = wt < lt ? wt : lt;
  int bottom = wb > lb ? wb : lb;

  if (top < adj->value)
    gtk_adjustment_set_value (adj, top);
  else if (bottom + 4 > adj->value + viewport->allocation.height)
    gtk_adjustment_set_value (adj, bottom - viewport->allocation.height + 4);
  
  return FALSE;
}

static gboolean put_content (GNode *node, gpointer data)
{
  pref_t *pref = node->data;
  pref_t *parent = node->parent ? node->parent->data : NULL;

  if (pref->notebook)
  {
    /* we have a page */
    pref_t *child = g_node_first_child (node)->data;
    GtkWidget *widget;

    if (child->notebook)
    {
      /* child pages present - create a notebook */
      widget = pref->page.notebook = pref->page.page = gtk_notebook_new ();
      gtk_notebook_set_scrollable (GTK_NOTEBOOK(widget), TRUE);
    }
    else
    {
      /* child pages not present - create a scrollable table */
      widget = pref->page.table = gtk_table_new (1, 1, FALSE);
      pref->page.page = gtk_scrolled_window_new (NULL, NULL);
      gtk_scrolled_window_set_policy
	(GTK_SCROLLED_WINDOW(pref->page.page),
			     GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
      gtk_scrolled_window_add_with_viewport
	(GTK_SCROLLED_WINDOW(pref->page.page), widget);
    }

    if (parent)
    {
      /* add this page to its parent's notebook */
      gtk_widget_ref (pref->page.page); /* hiding is done by removing */
      gtk_notebook_append_page (GTK_NOTEBOOK(parent->notebook), pref->page.page,
				gtk_label_new (pref->page.label));
    }
    else
      gtk_widget_set_size_request (pref->page.page, 600, 350);
  }
  else
  {
    /* we have a config item */
    int r = g_node_child_position (node->parent, node) * 2;
    GtkTable *table = GTK_TABLE(parent->page.table);
    gtk_table_attach (table, pref->item.editable, 0, 1, r, r + 1,
		      GTK_EXPAND | GTK_FILL, GTK_SHRINK, 2, 5);
    gtk_table_attach (table, pref->item.label, 1, 4, r, r + 1,
		      GTK_EXPAND | GTK_FILL, GTK_SHRINK, 5, 5);
    gtk_table_attach (table, pref->item.separator, 0, 4, r + 1, r + 2,
		      GTK_EXPAND | GTK_FILL, GTK_SHRINK | GTK_FILL, 5, 5);
    g_signal_connect (pref->item.editable, "focus",
		      focus_item_cb, node);
  }

  return FALSE;
}

static GtkWidget *make_prefs_window (void)
{
  xine_cfg_entry_t entry;
  gboolean done;

  if (!xine_config_get_first_entry (xine, &entry))
    return NULL;

  tooltips = gtk_tooltips_new ();
  prefs = create_page ("", 0); /* root page */

  do
  {
    const char *point;
    GNode *section;

    if (!entry.description ||
	!(point= strrchr (entry.key, '.')) || point == entry.key)
      continue;

    /* ensure that a page (window+table) is present for this config item */
    section = find_page (entry.key, point - entry.key);
    if (!section)
    {
      section = prefs;
      point = entry.key - 1;
      while ((point = strchr (point + 1, '.')))
      {
	GNode *self = find_page (entry.key, point - entry.key);
	if (!self)
	{
	  self = create_page (entry.key, point - entry.key);
	  g_node_append (section, self);
	}
	section = self;
      }
    }

    /* add the config item (and create its widgets) */
    g_node_append (section, create_item (&entry));
  } while (xine_config_get_next_entry (xine, &entry));

  /* we now have a full tree*/

  do
  {
    done = TRUE;
    g_node_traverse (prefs, G_PRE_ORDER, G_TRAVERSE_ALL, -1,
		     (GNodeTraverseFunc) unmix_mixed_page, &done);
  } while (!done);

  g_node_traverse (prefs, G_PRE_ORDER, G_TRAVERSE_ALL, -1, put_content, NULL);

  gtk_tooltips_enable (tooltips);
  return PREF_PAGE_T(prefs)->page;
}

static void response_cb (GtkDialog *dbox, int response, gpointer data)
{
  gchar *fname;
  switch (response)
  {
  case GTK_RESPONSE_ACCEPT:
    fname = get_config_filename (FILE_CONFIG);
    xine_config_save (xine, fname);
    g_free (fname);
    break;
  default:
    is_visible = 0;
    gtk_widget_hide (prefs_dialog);
  }
}

static JSBool js_preferences_show (JSContext *cx, JSObject *obj, uintN argc,
				   jsval *argv, jsval *rval)
{
  /* se_t *se = (se_t *) JS_GetContextPrivate(cx); */
  se_log_fncall_checkinit ("preferences_show");
  preferences_show ();
  return JS_TRUE;
}

void preferences_show (void)
{
  if (is_visible)
  {
    is_visible = FALSE;
    gtk_widget_hide (prefs_dialog);
  }
  else
  {
    is_visible = TRUE;
    window_show (prefs_dialog, NULL);
  }
}

void bowdlerpref_show (void)
{
  if (is_visible)
  {
    is_visible = FALSE;
    gtk_widget_hide (dialog_dialog);
  }
  else
  {
    is_visible = TRUE;
    window_show (dialog_dialog, NULL);
  }
}

void bowdlerpref_init(void)
{
	bowdler_dialog = gtk_dialog_new_with_buttons(_("Bowdler Preferences"), NULL, 0,
				GTK_STOCK_SAVE, GTK_RESPONSE_ACCEPT,
				GTK_STOCK_CLOSE, GTK_RESPONSE_DELETE_EVENT,
				NULL);

	gtk_window_set_default_size (GTK_WINDOW (bowdler_dialog), 500, 150);
        hide_on_delete (bowdler_dialog, &is_visible);
}

void preferences_init (void)
{
  xine_cfg_entry_t entry;
  GtkWidget *w;

  prefs_dialog = gtk_dialog_new_with_buttons (_("Preferences"), NULL, 0,
				GTK_STOCK_SAVE, GTK_RESPONSE_ACCEPT,
				GTK_STOCK_CLOSE, GTK_RESPONSE_DELETE_EVENT,
				NULL);
  gtk_window_set_default_size (GTK_WINDOW (prefs_dialog), 500, 150);
  hide_on_delete (prefs_dialog, &is_visible);
  g_signal_connect (G_OBJECT(prefs_dialog), "response",
		    G_CALLBACK(response_cb), NULL);

  /* Make new tabbed box (notebook) */
  gtk_box_pack_start_defaults (GTK_BOX(GTK_DIALOG(prefs_dialog)->vbox),
			       make_prefs_window ());

  w = gtk_label_new (_("Items marked “<b>• <i>like this</i></b>” require "
		       "gxine to be restarted to take effect.")),
  gtk_label_set_use_markup (GTK_LABEL(w), TRUE);
  gtk_box_pack_end (GTK_BOX(GTK_DIALOG(prefs_dialog)->vbox), w,
		    FALSE, FALSE, 2);
  gtk_widget_show_all (GTK_DIALOG(prefs_dialog)->vbox);

  if (xine_config_lookup_entry (xine, "gui.experience_level", &entry))
  {
    static int value;
    value = entry.num_value;
    g_idle_add ((GSourceFunc) select_show_pref_widgets, &value);
  }

  is_visible = 0;

  /* script engine functions */

  se_defun (gse, NULL, "preferences_show", js_preferences_show, 0, 0,
	    SE_GROUP_DIALOGUE, NULL, NULL);
}

void preferences_update_entry (const xine_cfg_entry_t *entry)
{
  gpointer widget = g_datalist_get_data (&prefs_map, entry->key);

  if (widget)
    switch (entry->type)
    {
    case XINE_CONFIG_TYPE_ENUM:
      gtk_combo_box_set_active (widget, entry->num_value);
      break;

    case XINE_CONFIG_TYPE_STRING:
      gtk_entry_set_text (widget, entry->str_value);
      break;

    case XINE_CONFIG_TYPE_RANGE: /* slider */
      gtk_range_set_value (widget, entry->num_value);
      break;

    case XINE_CONFIG_TYPE_NUM:
      gtk_spin_button_set_value (widget, entry->num_value);
      break;

    case XINE_CONFIG_TYPE_BOOL:
      gtk_toggle_button_set_active (widget, entry->num_value == 1);
      break;

    default:;
    }

  xine_config_update_entry (xine, entry);
}
